﻿using Microsoft.AspNetCore.Identity;
using Abp.Authorization;
using Abp.Authorization.Users;
using Abp.Configuration;
using Abp.Configuration.Startup;
using Abp.Dependency;
using Abp.Domain.Repositories;
using Abp.Domain.Uow;
using Abp.Zero.Configuration;
using AVISOutBound.SSOServer.Authorization.Roles;
using AVISOutBound.SSOServer.Authorization.Users;
using AVISOutBound.SSOServer.MultiTenancy;
using System.Threading.Tasks;
using System.Transactions;
using Abp.Extensions;
using System;

namespace AVISOutBound.SSOServer.Authorization
{
    public class LogInManager : AbpLogInManager<Tenant, Role, User>
    {
        private readonly UserManager _userManager;
        private readonly IRepository<User, long> _userRepository;
        public LogInManager(
            UserManager userManager,
            IMultiTenancyConfig multiTenancyConfig,
            IRepository<Tenant> tenantRepository,
            IUnitOfWorkManager unitOfWorkManager,
            ISettingManager settingManager,
            IRepository<UserLoginAttempt, long> userLoginAttemptRepository,
            IUserManagementConfig userManagementConfig,
            IIocResolver iocResolver,
            IPasswordHasher<User> passwordHasher,
            RoleManager roleManager,
            IRepository<User, long> userRepository,
            UserClaimsPrincipalFactory claimsPrincipalFactory)
            : base(
                  userManager,
                  multiTenancyConfig,
                  tenantRepository,
                  unitOfWorkManager,
                  settingManager,
                  userLoginAttemptRepository,
                  userManagementConfig,
                  iocResolver,
                  passwordHasher,
                  roleManager,
                  claimsPrincipalFactory)
        {
            _userManager = userManager;
            _userRepository = userRepository;
        }
        protected virtual async Task SaveLoginAttemptByPhone(AbpLoginResult<Tenant, User> loginResult, string tenancyName, string phone)
        {
            using (var uow = UnitOfWorkManager.Begin(TransactionScopeOption.Suppress))
            {
                var tenantId = loginResult.Tenant != null ? loginResult.Tenant.Id : (int?)null;
                using (UnitOfWorkManager.Current.SetTenantId(tenantId))
                {
                    var loginAttempt = new UserLoginAttempt
                    {
                        TenantId = tenantId,
                        TenancyName = tenancyName,

                        UserId = loginResult.User != null ? loginResult.User.Id : (long?)null,
                        UserNameOrEmailAddress = phone,

                        Result = loginResult.Result,

                        BrowserInfo = ClientInfoProvider.BrowserInfo,
                        ClientIpAddress = ClientInfoProvider.ClientIpAddress,
                        ClientName = ClientInfoProvider.ComputerName,
                    };

                    await UserLoginAttemptRepository.InsertAsync(loginAttempt);
                    await UnitOfWorkManager.Current.SaveChangesAsync();

                    await uow.CompleteAsync();
                }
            }
        }
        [UnitOfWork]
        public virtual async Task<AbpLoginResult<Tenant, User>> LoginAsyncByPhoneAndPwd(string phone, string plainPassword, string tenancyName = null, bool shouldLockout = false)
        {
            var result = await LoginAsyncInternalByPhone(phone, plainPassword, tenancyName, shouldLockout);
            await SaveLoginAttemptByPhone(result, tenancyName, phone);
            return result;
        }

        protected virtual async Task<AbpLoginResult<Tenant, User>> LoginAsyncInternalByPhone(string phone, string plainPassword, string tenancyName, bool shouldLockout)
        {
            if (phone.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(phone));
            }

            if (plainPassword.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(plainPassword));
            }

            //Get and check tenant
            Tenant tenant = null;
            using (UnitOfWorkManager.Current.SetTenantId(null))
            {
                if (!MultiTenancyConfig.IsEnabled)
                {
                    tenant = await GetDefaultTenantAsync();
                }
                else if (!string.IsNullOrWhiteSpace(tenancyName))
                {
                    tenant = await TenantRepository.FirstOrDefaultAsync(t => t.TenancyName == tenancyName);
                    if (tenant == null)
                    {
                        return new AbpLoginResult<Tenant, User>(AbpLoginResultType.InvalidTenancyName);
                    }

                    if (!tenant.IsActive)
                    {
                        return new AbpLoginResult<Tenant, User>(AbpLoginResultType.TenantIsNotActive, tenant);
                    }
                }
            }

            var tenantId = tenant == null ? (int?)null : tenant.Id;
            using (UnitOfWorkManager.Current.SetTenantId(tenantId))
            {
                await UserManager.InitializeOptionsAsync(tenantId);

                //TryLoginFromExternalAuthenticationSources method may create the user, that's why we are calling it before AbpStore.FindByNameOrEmailAsync
                var loggedInFromExternalSource = false;

                var user = await _userManager.FindByNameOrEmailAsyncByPhone(tenantId, phone);
                if (user == null)
                {
                    return new AbpLoginResult<Tenant, User>(AbpLoginResultType.InvalidUserNameOrEmailAddress, tenant);
                }

                if (await UserManager.IsLockedOutAsync(user))
                {
                    return new AbpLoginResult<Tenant, User>(AbpLoginResultType.LockedOut, tenant, user);
                }

                if (!loggedInFromExternalSource)
                {
                    if (!await UserManager.CheckPasswordAsync(user, plainPassword))
                    {
                        if (shouldLockout)
                        {
                            if (await TryLockOutAsync(tenantId, user.Id))
                            {
                                return new AbpLoginResult<Tenant, User>(AbpLoginResultType.LockedOut, tenant, user);
                            }
                        }

                        return new AbpLoginResult<Tenant, User>(AbpLoginResultType.InvalidPassword, tenant, user);
                    }

                    await UserManager.ResetAccessFailedCountAsync(user);
                }

                return await CreateLoginResultAsync(user, tenant);
            }
        }
        public virtual async Task<LoginAsyncByPhoneAndSmsCodeResult> LoginAsyncByPhone(string phone, string tenancyName = null, bool shouldLockout = false)
        {
            var result = await LoginAsyncInternalByPhone(phone, tenancyName, shouldLockout);
            await SaveLoginAttemptByPhone(result.AbpLoginResult, tenancyName, phone);
            return result;
        }

        protected virtual async Task<LoginAsyncByPhoneAndSmsCodeResult> LoginAsyncInternalByPhone(string phone, string tenancyName, bool shouldLockout)
        {
            if (phone.IsNullOrEmpty())
            {
                throw new ArgumentNullException(nameof(phone));
            }
            //Get and check tenant
            Tenant tenant = null;
            using (UnitOfWorkManager.Current.SetTenantId(null))
            {
                if (!MultiTenancyConfig.IsEnabled)
                {
                    tenant = await GetDefaultTenantAsync();
                }
                else if (!string.IsNullOrWhiteSpace(tenancyName))
                {
                    tenant = await TenantRepository.FirstOrDefaultAsync(t => t.TenancyName == tenancyName);
                    if (tenant == null)
                    {
                        return GetResult(new AbpLoginResult<Tenant, User>(AbpLoginResultType.InvalidTenancyName));
                    }

                    if (!tenant.IsActive)
                    {
                        return GetResult(new AbpLoginResult<Tenant, User>(AbpLoginResultType.TenantIsNotActive, tenant));
                    }
                }
            }

            var tenantId = tenant == null ? (int?)null : tenant.Id;
            using (UnitOfWorkManager.Current.SetTenantId(tenantId))
            {
                await UserManager.InitializeOptionsAsync(tenantId);

                //TryLoginFromExternalAuthenticationSources method may create the user, that's why we are calling it before AbpStore.FindByNameOrEmailAsync
                var loggedInFromExternalSource = false;
                bool isRegUser = true;
                var user = await _userManager.FindByNameOrEmailAsyncByPhone(tenantId, phone);
                if (user == null)
                {
                    // return GetResult(new AbpLoginResult<Tenant, User>(AbpLoginResultType.InvalidUserNameOrEmailAddress, tenant));

                    User model = new User();
                    //todo 考虑判断手机号码真实性,错误手机号码是否会发送短信验证码成功扣费问题
                    model.PhoneNumber = phone;
                    model.IsActive = true;//先设置未启用，注册成功则启用
                    _userRepository.Insert(model);
                }
                if (user.IsActive == false)
                {
                    isRegUser = false;
                }
                if (await UserManager.IsLockedOutAsync(user))
                {
                    return GetResult(new AbpLoginResult<Tenant, User>(AbpLoginResultType.LockedOut, tenant, user));
                }

                if (!loggedInFromExternalSource)
                {
                    user.IsActive = true;
                    user.SmSCode = null;//登陆成功后清楚短信验证码
                    user.SmsCodeSendLockoutEndDateUtc = null;
                    await UserManager.ResetAccessFailedCountAsync(user);
                }

                return GetResult(await CreateLoginResultAsync(user, tenant), isRegUser);
            }
        }
        private LoginAsyncByPhoneAndSmsCodeResult GetResult(AbpLoginResult<Tenant, User> abpLoginResult, bool isRegUser = true)
        {
            return new LoginAsyncByPhoneAndSmsCodeResult { AbpLoginResult = abpLoginResult, IsRegUser = isRegUser };
        }
    }
    public class LoginAsyncByPhoneAndSmsCodeResult
    {
        public AbpLoginResult<Tenant, User> AbpLoginResult { get; set; }
        public bool IsRegUser { get; set; }
    }
}
